<?php
/**
 * CSVリーダ
 *
 * @author tanaka
 * @package File
 * @subpackage Csv_Reader
 */
class File_Csv_Reader
{
	/*
	 * 定数 ***********************************************************************************/

	/**
	 * 値の区切り文字
	 *
	 * @var string
	 */
	const VALUE_SEPARATOR = ",";

	/**
	 * CSVファイルのエンコード
	 * @var string
	 */
	const CSV_ENCODING = "sjis-win";

	/**
	 * エラーなし
	 *
	 * @var int
	 */
	const ERROR_NONE = 0;

	/**
	 * ファイルがない
	 *
	 * @var int
	 */
	const ERROR_FILE_NOT_EXIST = 1;

	/**
	 * ファイル読み込みエラー
	 *
	 * @var int
	 */
	const ERROR_FILE_READING = 2;

	/**
	 * 1行辺りのデータ数が合わない
	 *
	 * @var int
	 */
	const ERROR_CSV_LINE_DATA_COUNT = 3;

	/**
	 * アップロード時になんらかのエラー
	 * @var int
	 */
	const ERROR_ANYTHING_UPLOAD = 4;

	/**
	 * 読み込んだデータなし
	 * @var int
	 */
	const ERROR_READ_DATA_NONE = 5;

	/*
	 * 変数 ***********************************************************************************/

	/**
	 * エラー理由
	 * @var int
	 */
	protected $_error = self::ERROR_FILE_NOT_EXIST;

	/**
	 * 現在の行
	 *
	 * @var int
	 */
	protected $_currentLineNumber = 0;

	/**
	 * データ
	 *
	 * @var array
	 */
	protected $_data = array();

	/**
	 * 1行のデータ数
	 * @var int
	 */
	protected $_lineRowCount = 0;

	/**
	 * 行数
	 * @var int
	 */
	protected $_lineCount = 0;

	/*
	 * 関数 ***********************************************************************************/

	/**
	 * エラーの文言を取得します。
	 *
	 * @param File_Csv_Reader $csv
	 * @return
	 */
	public static function getCommonErrorReason($csv)
	{
		switch ($csv->getError()) {
			case self::ERROR_FILE_NOT_EXIST:
				return 'CSVファイルがアップロードされていません。';

			case self::ERROR_FILE_READING:
				return 'ファイルの読み込み時にエラーが発生しました。';

			case self::ERROR_CSV_LINE_DATA_COUNT:
				return $csv->getErrorLine() . '行目：CSVファイルのデータ数が正しくありません。';

			case self::ERROR_ANYTHING_UPLOAD:
				return 'アップロード時になんらかのエラーが発生しました。';

			case self::ERROR_READ_DATA_NONE:
				return 'CSVファイルにデータが存在しません。';

			default:
				return '';
		}
	}

	/**
	 * コンストラクタ
	 *
	 * @param int $targetFile
	 * @param bool optional ヘッダ行が存在するか
	 * @return void
	 */
	public function __construct($targetFile, $isExistHeaderLine = true)
	{
		$this->_isExistHeaderLine = $isExistHeaderLine;

		if (!file_exists($targetFile)) return;

		if (!is_readable($targetFile)) {
			$this->_error = self::ERROR_FILE_READING;
			return;
		}

		$data = array();
		$fp = fopen($targetFile, 'r');

		while (true) {

			if (feof($fp)) {
				break;
			}

			$line = fgets($fp);
			$line = $this->_getFixedLineData($line);
			$this->_currentLineNumber++;

			if (!strlen(trim(str_replace(self::VALUE_SEPARATOR, '', $line)))) {
				continue;
			}

			$lineData = explode(self::VALUE_SEPARATOR, $line);

			if ($this->_lineRowCount == 0) {
				$this->_lineRowCount = count($lineData);
			}

			if ($isExistHeaderLine) {
				$isExistHeaderLine = false;
				continue;
			}

			if (count($lineData) != $this->_lineRowCount) {
				$this->_error = self::ERROR_CSV_LINE_DATA_COUNT;
				return;
			}

			$fixed = $this->_getFixdRowData($lineData);

			if (is_null($fixed)) {
				$this->_error = self::ERROR_CSV_LINE_DATA_COUNT;
				return;
			}

			$data[$this->_currentLineNumber] = $fixed;
		}

		fclose($fp);

		if (count($data)) {
			$this->_data = $data;
			$this->_error = self::ERROR_NONE;
			$this->_lineCount = $this->_currentLineNumber;
			$this->resetCurrentLine();
			return;
		}

		$this->_error = self::ERROR_READ_DATA_NONE;
	}

	/**
	 * 取得した行データの調節
	 * @param string $line
	 * @return string
	 */
	protected function _getFixedLineData($line)
	{
		$line = str_replace(array("\r\n", "\n", "\r"), '', $line);
		return $data = mb_convert_encoding(trim($line), 'utf-8', self::CSV_ENCODING);
	}

	/**
	 * 配列の値を調節します。
	 * 不正な配列の場合nullを返す。
	 *
	 * @return array|null
	 */
	protected function _getFixdRowData($row)
	{
		// トリム処理
		foreach ($row as $key => $value) {
			$row[$key] = trim($value);
		}

		return $row;
	}

	/**
	 * エラー理由を取得
	 *
	 * @return int
	 */
	public function getError()
	{
		return $this->_error;
	}

	/**
	 * エラーかどうか返します。
	 *
	 * @return
	 */
	public function isValid()
	{
		return self::ERROR_NONE == $this->getError();
	}

	/**
	 * エラー行を取得します。
	 *
	 * @return
	 */
	public function getErrorLine()
	{
		if ($this->isValid()) {
			return null;
		}

		return $this->getCurrentLineNumber();
	}

	/**
	 * 現在参照中の行を返します。
	 *
	 * @return int
	 */
	public function getCurrentLineNumber()
	{
		return $this->_currentLineNumber;
	}

	/**
	 * すべての行を取得します。
	 *
	 * @return array
	 */
	public function getAllData()
	{
		return $this->_data;
	}

	/**
	 * 行の参照状況をリセットします。
	 *
	 * @return
	 */
	public function resetCurrentLine()
	{
		$this->_currentLineNumber = 0;
	}

	/**
	 * 1行取り出します。
	 *
	 * @return null|array
	 */
	public function getLineData()
	{
		while (true) {
			if ($this->_currentLineNumber > $this->_lineCount) return null;

			$this->_currentLineNumber++;

			if (isset($this->_data[$this->_currentLineNumber])) {
				return $this->_data[$this->_currentLineNumber];
			}
		}
	}

	/**
	 * 1列取り出します。
	 *
	 * @param string $columnName 連想配列のキー
	 * @param bool optional $isDistinct 既に同じ値があった場合無視 デフォルト：ture
	 * @return array array(行番号 => 値)
	 */
	public function getColumnData($columnName, $isDistinct = true)
	{
		$result = array();
		foreach ($this->getAllData() as $lineNumber => $data) {

			if (!isset($data[$columnName]) || !strlen($data[$columnName])) {
				continue;
			}

			if (!$isDistinct) {
				$result[$lineNumber] = $data[$columnName];
				continue;
			}

			if (!in_array($data[$columnName], $result)) {
				$result[$lineNumber] = $data[$columnName];
			}
		}

		return $result;
	}

	/**
	 * 重複カラムがあった場合返します。
	 *
	 * @param string|array $key 重複チェックをするキー名 配列の場合２列以上にまたがる重複をチェック
	 * @return array|false array(２つの重複している列番号)
	 */
	public function getDuplicateLine($key)
	{
		$collection = array();

		if (is_array($key)) {

			$keys = $key;

			foreach ($this->getAllData() as $lineNumber => $data) {
				$current = array();

				foreach ($keys as $key) {
					if (!isset($data[$key]) || !strlen($data[$key])) {
						continue;
					}

					$current[$key] = $data[$key];
				}

				if (in_array($current, $collection)) {
					foreach ($collection as $duplicateLine => $duplicateData) {
						if ($duplicateData == $current) {
							return array($duplicateLine, $lineNumber);
						}
					}
				}

				$collection[$lineNumber] = $current;
			}

			return false;
		}

		foreach ($this->getAllData() as $lineNumber => $data) {

			if (!isset($data[$key]) || !strlen($data[$key])) {
				continue;
			}

			if (in_array($data[$key], $collection)) {
				foreach ($collection as $duplicateLine => $duplicateData) {
					if ($duplicateData == $data[$key]) {
						return array($duplicateLine, $lineNumber);
					}
				}
			}

			$collection[$lineNumber] = $data[$key];
		}

		return false;
	}
}
